#include "str.h"
#include "strescape.h"
#include "time-util.h"
+#include "unichar.h"
#include "env-util.h"
#include "settings.h"
#include "ssl-settings.h"
#define DB_LDAP_REQUEST_MAX_ATTEMPT_COUNT 3
#define DB_LDAP_ATTR_DN "~dn"
-static const char *LDAP_ESCAPE_CHARS = "*,\\#+<>;\"()= ";
-
struct db_ldap_result {
int refcount;
LDAPMessage *msg;
*sensitive_r = array_front(&sensitive_attr_names);
}
-#define IS_LDAP_ESCAPED_CHAR(c) \
- ((((unsigned char)(c)) & 0x80) != 0 || strchr(LDAP_ESCAPE_CHARS, (c)) != NULL)
-
-const char *ldap_escape(const char *str,
- void *context ATTR_UNUSED)
+const char *ldap_escape(const char *input, void *context ATTR_UNUSED)
{
- string_t *ret = NULL;
+ /* This function escapes both LDAP filters and LDAP DNs. This works,
+ because both allow using the method of escaping any characters.
+ However, this isn't really recommended, since apparently some LDAP
+ servers and perhaps other tools don't handle unnecessary escaping
+ correctly.
+
+ Another issue is what to do with invalid UTF-8. LDAP filter RFC
+ suggests just escaping invalid UTF-8. LDAP DN RFC doesn't at least
+ explicitly say that. It instead seems to imply that the string is
+ just expected to be valid UTF-8. Or perhaps to use #hex-string
+ encoding, which isn't compatible with LDAP filters. Just to be safe,
+ we'll replace invalid UTF-8 input with the UTF-8 replacement
+ character.
+
+ So this function isn't perhaps the most standards compliant one, but
+ we don't really care, since this escaping is mainly intended to
+ prevent untrusted username input from breaking out of the filter or
+ DN. Valid usernames are unlikely to require escaping or to be
+ invalid UTF-8. */
+
+ /* Convert invalid UTF-8 characters to replacement characters. */
+ size_t input_len = strlen(input);
+ string_t *str = t_str_new(64);
+ if (!uni_utf8_get_valid_data((const unsigned char *)input,
+ input_len, str)) {
+ /* Input was not valid UTF-8 */
+ input = t_strdup(str_c(str));
+ input_len = str_len(str);
+ str_truncate(str, 0);
+ }
- for (const char *p = str; *p != '\0'; p++) {
- if (IS_LDAP_ESCAPED_CHAR(*p)) {
- if (ret == NULL) {
- ret = t_str_new((size_t) (p - str) + 64);
- str_append_data(ret, str, (size_t) (p - str));
- }
- str_printfa(ret, "\\%02X", (unsigned char)*p);
- } else if (ret != NULL)
- str_append_c(ret, *p);
+ const char *escape_chars =
+ /* control characters */
+ "\x01\x02\x03\x04\x05\x06\x07\x08"
+ "\x09\x0a\x0b\x0c\x0d\x0e\x0f\x10"
+ "\x11\x12\x03\x14\x15\x16\x17\x18"
+ "\x19\x1a\x1b\x1c\x1d\x1e\x1f"
+ /* filter + DN */
+ "\\"
+ /* filter only */
+ "*()"
+ /* DN only (not including leading and trailing chars) */
+ ",+<>;\"=";
+ size_t pos;
+
+ /* check the leading character separately (required by DN) */
+ if (input[0] == '#' || input[0] == ' ')
+ pos = 0;
+ else {
+ pos = strcspn(input, escape_chars);
+ if (pos == input_len) {
+ /* the last trailing space is escaped
+ (required by DN) */
+ if (pos > 0 && input[pos - 1] == ' ')
+ pos--;
+ else
+ return input;
+ }
}
- return ret == NULL ? str : str_c(ret);
+ do {
+ str_append_data(str, input, pos);
+ str_printfa(str, "\\%02x", (unsigned char)input[pos]);
+ input += pos + 1;
+ input_len -= pos + 1;
+ pos = strcspn(input, escape_chars);
+ /* the last trailing space is escaped (required by DN) */
+ if (pos == input_len && pos > 0 && input[pos - 1] == ' ')
+ pos--;
+ } while (pos < input_len);
+ str_append_data(str, input, pos);
+ return str_c(str);
}
static bool
--- /dev/null
+/* Copyright (c) 2026 Dovecot authors, see the included COPYING file */
+
+#include "test-auth.h"
+
+#ifdef HAVE_LDAP
+#include "db-ldap.h"
+
+static void test_ldap_escape(void)
+{
+ struct {
+ const char *input;
+ const char *output;
+ } tests[] = {
+ { "", "" },
+ { " ", "\\20" },
+ { " ", "\\20\\20" },
+ { "foo ", "foo\\20" },
+ { ",", "\\2c" },
+ { "s p a c e", "s p a c e" },
+ { "# start-end#", "\\23 start-end#" },
+ { " start-end2 ", "\\20 start-end2 \\20" },
+ { "middle:,+\"\\<>;=", "middle:\\2c\\2b\\22\\5c\\3c\\3e\\3b\\3d" },
+ { "valid-utf8:\xc3\xb1", "valid-utf8:\xc3\xb1" },
+ { "Bad \xFF Characters", "Bad \xEF\xBF\xBD Characters" },
+ };
+ test_begin("ldap_escape()");
+ for (unsigned int i = 0; i < N_ELEMENTS(tests); i++) {
+ test_assert_strcmp_idx(ldap_escape(tests[i].input, NULL),
+ tests[i].output, i);
+ }
+ test_end();
+}
+
+void test_db_ldap(void)
+{
+ test_ldap_escape();
+}
+
+#endif